/*
 * Copyright 2017-2022 original authors
 *
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 * https://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
package io.micronaut.core.graal;

import io.micronaut.core.annotation.AnnotationMetadata;
import io.micronaut.core.annotation.AnnotationMetadataProvider;
import io.micronaut.core.annotation.AnnotationValue;
import io.micronaut.core.annotation.Internal;
import org.jspecify.annotations.NonNull;
import org.jspecify.annotations.Nullable;
import io.micronaut.core.annotation.ReflectionConfig;
import io.micronaut.core.annotation.TypeHint;
import io.micronaut.core.util.CollectionUtils;

import java.lang.reflect.Constructor;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.List;
import java.util.Optional;
import java.util.Set;

/**
 * Interface that allows dynamic configuration of reflection generated by the GraalTypeElementVisitor.
 *
 * @author graemerocher
 * @since 3.5.0
 * @see io.micronaut.core.annotation.ReflectionConfig
 */
@Internal
public interface GraalReflectionConfigurer extends AnnotationMetadataProvider {
    /**
     * The suffix used for generated classes.
     */
    String CLASS_SUFFIX = "$ReflectConfig";

    /**
     * Configure reflection for this type.
     * @param context The feature access
     */
    @SuppressWarnings({"unused", "java:S3776"})
    default void configure(ReflectionConfigurationContext context) {
        final AnnotationMetadata annotationMetadata = getAnnotationMetadata();
        final List<AnnotationValue<ReflectionConfig>> values = annotationMetadata.getAnnotationValuesByType(
                ReflectionConfig.class);
        for (AnnotationValue<ReflectionConfig> reflectConfig : values) {
            reflectConfig.stringValue("type").ifPresent(className -> {
                Class<?> t = context.findClassByName(className);
                if (t == null) {
                    return;
                }
                context.register(t);
                TypeHint.AccessType[] accessTypes = reflectConfig.enumValues("accessType", TypeHint.AccessType.class);
                // DO NOT change to Set.of(..) which disallows duplicates
                final Set<TypeHint.AccessType> accessType = CollectionUtils.setOf(
                    accessTypes
                );
                if (accessType.contains(TypeHint.AccessType.DYNAMIC_PROXY)) {
                    context.registerDynamicProxy(t);
                }
                if (accessType.contains(TypeHint.AccessType.ALL_PUBLIC_METHODS)) {
                    final Method[] methods = t.getMethods();
                    for (Method method : methods) {
                        if (Modifier.isPublic(method.getModifiers())) {
                            context.register(method);
                        }
                    }
                }
                if (accessType.contains(TypeHint.AccessType.ALL_DECLARED_METHODS)) {
                    final Method[] declaredMethods = t.getDeclaredMethods();
                    context.register(declaredMethods);
                }
                if (accessType.contains(TypeHint.AccessType.ALL_PUBLIC_FIELDS)) {
                    final Field[] fields = t.getFields();
                    for (Field field : fields) {
                        if (Modifier.isPublic(field.getModifiers())) {
                            context.register(field);
                        }
                    }
                }
                if (accessType.contains(TypeHint.AccessType.ALL_DECLARED_FIELDS)) {
                    final Field[] fields = t.getDeclaredFields();
                    context.register(fields);
                }
                if (accessType.contains(TypeHint.AccessType.ALL_PUBLIC_CONSTRUCTORS)) {
                    final Constructor<?>[] constructors = t.getConstructors();
                    for (Constructor<?> constructor : constructors) {
                        if (Modifier.isPublic(constructor.getModifiers())) {
                            context.register(constructor);
                        }
                    }
                }
                if (accessType.contains(TypeHint.AccessType.ALL_DECLARED_CONSTRUCTORS)) {
                    final Constructor<?>[] constructors = t.getDeclaredConstructors();
                    context.register(constructors);
                }

                final List<AnnotationValue<ReflectionConfig.ReflectiveMethodConfig>> methodConfig =
                        reflectConfig.getAnnotations("methods", ReflectionConfig.ReflectiveMethodConfig.class);
                for (AnnotationValue<ReflectionConfig.ReflectiveMethodConfig> mrc :
                        methodConfig) {
                    mrc.stringValue("name").ifPresent(n -> {
                        final String[] typeNames = mrc.stringValues("parameterTypes");
                        final Class<?>[] parameterTypes = new Class<?>[typeNames.length];
                        for (int i = 0; i < typeNames.length; i++) {
                            String typeName = typeNames[i];
                            final Class<?> pt = context.findClassByName(typeName);
                            if (pt == null) {
                                // bail out
                                return;
                            } else {
                                parameterTypes[i] = pt;
                            }
                        }
                        if (n.equals("<init>")) {
                            try {
                                Constructor<?> c = t.getDeclaredConstructor(parameterTypes);
                                context.register(c);
                            } catch (NoSuchMethodException e) {
                                // ignore
                            }
                        } else {
                            try {
                                Method method = t.getDeclaredMethod(n, parameterTypes);
                                context.register(method);
                            } catch (NoSuchMethodException e) {
                                // ignore
                            }
                        }
                    });
                }

                final List<AnnotationValue<ReflectionConfig.ReflectiveFieldConfig>> fields =
                        reflectConfig.getAnnotations(
                        "fields",
                        ReflectionConfig.ReflectiveFieldConfig.class
                );

                for (AnnotationValue<ReflectionConfig.ReflectiveFieldConfig> field : fields) {
                    field.stringValue("name")
                            .flatMap(n -> {
                                try {
                                    return Optional.of(t.getDeclaredField(n));
                                } catch (NoSuchFieldException e) {
                                    return Optional.empty();
                                }
                            })
                            .ifPresent(context::register);
                }
            });
        }
    }

    /**
     * Context object for the configuration.
     */
    interface ReflectionConfigurationContext {
        /**
         * Finds a class by name.
         * @param name The name
         * @return The class or null
         */
        @Nullable
        Class<?> findClassByName(@NonNull String name);

        /**
         * Register the given types for reflection.
         * @param types The types
         */
        void register(Class<?>... types);

        /**
         * Register the given methods for reflection.
         * @param methods The methods
         */
        void register(Method... methods);

        /**
         * Register the given fields for reflection.
         * @param fields The fields
         */
        void register(Field... fields);

        /**
         * Register the given constructors for reflection.
         * @param constructors The constructors
         */
        void register(Constructor<?>... constructors);

        /**
         * Register a dynamic proxy.
         * @param proxyClass The proxy class
         */
        void registerDynamicProxy(Class<?> proxyClass);
    }
}
